import { MainClient, USDMClient, CoinMClient } from "binance";
import { errLogger, outLogger } from "../common/logger";
import { retryPromise } from "../common/retry";
import { autoToFixed, decimalZeroDigitsReg, singlePromise } from "../utils";
import { BinanceWebSocket } from './BinanceWebSocket';
import { ONLINE } from "../constants/common";
import { getSpotSymbol, parserBaseSymbol } from "../trader";

export interface SymbolInfo {
  filterType: string
  // minNotional = minimum order value (price * quantity)
  minNotional: string
  minPrice: string
  maxPrice: string
  tickSize: string
  stepSize: string
  minQty: string
  maxQty: string
  baseAssetPrecision: number
  quoteAssetPrecision: number
}
export interface FuturesSymbolInfo {
  minPrice: string
  maxPrice: string
  tickSize: string
  stepSize: string
  minQty: string
  maxQty: string
  pricePrecision: number
  quantityPrecision: number
}

// symbol: 'BNBUSD_230630',
// pair: 'BNBUSD',
// baseAsset: 'BNB',
// quoteAsset: 'USD',
export interface DeliverySymbolInfo extends FuturesSymbolInfo {
  pair: string
  symbol: string
  baseAsset: string
  quoteAsset: string
}

export class BinanceCommon {
  /** 
  * @see https://github.com/tiagosiebler/binance/blob/master/src/main-client.ts
  */
  mainClient: MainClient
  /** 
   * @see https://github.com/tiagosiebler/binance/blob/master/src/usdm-client.ts
   */
  futureClient: USDMClient
  /** 
   * @see https://github.com/tiagosiebler/binance/blob/master/src/coinm-client.ts
   */
  deliveryClient: CoinMClient
  /**
   * 缓存币的基本信息
   */
  symbols: Record<string, SymbolInfo> = {};
  futuresSymbols: Record<string, FuturesSymbolInfo> = {};
  deliverySymbols: Record<string, DeliverySymbolInfo> = {};

  futuresPrices: Record<string, number> = {};
  binanceWebSocket: BinanceWebSocket;
  constructor() {
    this.mainClient = new MainClient();
    this.futureClient = new USDMClient();
    this.deliveryClient = new CoinMClient();
    this.binanceWebSocket = new BinanceWebSocket();

    if (ONLINE) {
      this.init();
    }
  }
  async init() {
    try {
      await Promise.all([
        this.getSymbolInfo(),
        this.getFuturesSymbolInfo(),
        this.getDeliverySymolInfo(),
      ])
    } catch (error) {
      errLogger.error(error);
    }
    outLogger.info('BinanceCommon init');
  }
  updateSDK({ access_key, secret_key, }: { access_key: string; secret_key: string; }) {
    this.mainClient = new MainClient({
      api_key: access_key,
      api_secret: secret_key,
    });
    this.futureClient = new USDMClient({
      api_key: access_key,
      api_secret: secret_key,
    });
    this.deliveryClient = new CoinMClient({
      api_key: access_key,
      api_secret: secret_key,
    });
  }
  /**
   * 持仓价值(usdt)
   */
  calculationDeliveryNotional = (symbol: string) => {
    const map = {
      BTC: 100,
    }
    const base = parserBaseSymbol(getSpotSymbol(symbol))
    const multiple = map[base] ?? 10
    return multiple
  }
  /**
   * 币本位，张 -> 币单位
   */
  countToBaseSymbolCount = (symbol: string, price: number | string, amount: number | string) => {
    return (this.calculationDeliveryNotional(symbol) *  Number(amount)) / Number(price);
  }
  /**
   * 币本位，币单位 -> 张, 取整
   */
  baseCountToCount = (symbol: string, price: number | string, count: number) => {
    return Math.floor(Number(price) * count / this.calculationDeliveryNotional(symbol));
  }

  async getSymbolInfo<S extends string>(symbol?: S): Promise<SymbolInfo | undefined> {

    if (symbol && this.symbols[symbol as string]) {
      return this.symbols[symbol as string] as SymbolInfo;
    }
    try {
      const { symbols: symbolsList } = await retryPromise(2, () => this.mainClient.getExchangeInfo());

      for (let i = 0; i < symbolsList.length; i++) {
        const symbolInfo = symbolsList[i];
        const filters: Partial<SymbolInfo> = {}
        for (const filter of symbolInfo.filters) {
          if (filter.filterType == "MIN_NOTIONAL") {
            // filters.minNotional = filter.notional;
          } else if (filter.filterType == "PRICE_FILTER") {
            filters.minPrice = filter.minPrice as string;
            filters.maxPrice = filter.maxPrice as string;
            filters.tickSize = filter.tickSize as string;
          } else if (filter.filterType == "LOT_SIZE") {
            filters.stepSize = filter.stepSize as string;
            filters.minQty = filter.minQty as string;
            filters.maxQty = filter.maxQty as string;
          }
        }
        filters.baseAssetPrecision = symbolInfo.baseAssetPrecision;
        filters.quoteAssetPrecision = symbolInfo.quoteAssetPrecision;

        this.symbols[symbolInfo.symbol] = filters as SymbolInfo;
      }
      return symbol ? this.symbols[symbol] : undefined;
    } catch (error) {
      outLogger.error('getSymbolInfo', error.message)
    }
  }
  async getFuturesSymbolInfo<S extends string>(symbol?: S): Promise<FuturesSymbolInfo> {
    if (symbol && this.futuresSymbols[symbol as string]) {
      return this.futuresSymbols[symbol as string] as FuturesSymbolInfo;
    }
    try {
      const { symbols: symbolsList } = await retryPromise(2, () => this.futureClient.getExchangeInfo());
      for (let i = 0; i < symbolsList.length; i++) {
        const symbolInfo = symbolsList[i];
        const filters: Partial<FuturesSymbolInfo> = {};
        for (const filter of symbolInfo.filters) {
          if (filter.filterType == "MIN_NOTIONAL") {
            // filters.minNotional = filter.notional;
          } else if (filter.filterType == "PRICE_FILTER") {
            filters.minPrice = filter.minPrice as string;
            filters.maxPrice = filter.maxPrice as string;
            filters.tickSize = filter.tickSize as string;
          } else if (filter.filterType == "LOT_SIZE") {
            filters.stepSize = filter.stepSize as string;
            filters.minQty = filter.minQty as string;
            filters.maxQty = filter.maxQty as string;
          }
        }
        filters.pricePrecision = symbolInfo.pricePrecision;
        filters.quantityPrecision = symbolInfo.quantityPrecision;
        // 修正价格精度
        if (filters.tickSize) {
          const match = filters.tickSize.match(decimalZeroDigitsReg);
          const tickSizeDigit = match ? match[2].length + 1 : -1;
          filters.pricePrecision =
            tickSizeDigit !== -1 &&
              filters.pricePrecision > tickSizeDigit
              ? tickSizeDigit
              : filters.pricePrecision;
        }
        this.futuresSymbols[symbolInfo.symbol] = filters as FuturesSymbolInfo;
      }
      return this.futuresSymbols[symbol]
    } catch (error) {
      outLogger.error('getFuturesSymbolInfo', error.message)
    }
  }
  /** chache数据已处理： symbol: 'BTCUSD_240329' -> symbol: 'BTCUSDT' */
  async getDeliverySymolInfo<S extends string>(symbol?: S): Promise<DeliverySymbolInfo> {
    const deliverySymbol = symbol ? symbol : undefined;
    if (Object.keys(this.deliverySymbols).length > 0) {
      if (this.deliverySymbols[deliverySymbol]) {
        return this.deliverySymbols[deliverySymbol];
      } else {
        for (const key in this.deliverySymbols) {
          if (Object.prototype.hasOwnProperty.call(this.deliverySymbols, key)) {
            const {pair} = this.deliverySymbols[key];
            if (symbol.includes(pair)) {
              return this.deliverySymbols[key];
            }
          }
        }
      }
    }
    
    try {
      const { symbols } = await this.deliveryClient.getExchangeInfo();
      for (let i = 0; i < symbols.length; i++) {
        const symbolInfo = symbols[i];

        if (symbolInfo.contractType !== 'NEXT_QUARTER') {
          continue
        }
        const filters: Partial<DeliverySymbolInfo> = {
          symbol: symbolInfo.symbol,
          pair: symbolInfo.pair,
          pricePrecision: symbolInfo.pricePrecision,
          quantityPrecision: symbolInfo.quantityPrecision,
          baseAsset: symbolInfo.baseAsset,
          quoteAsset: symbolInfo.quoteAsset,
        }
        for (const filter of symbolInfo.filters) {
          if (filter.filterType == "MIN_NOTIONAL") {
            // filters.minNotional = filter.notional;
          } else if (filter.filterType == "PRICE_FILTER") {
            filters.minPrice = filter.minPrice as string;
            filters.maxPrice = filter.maxPrice as string;
            filters.tickSize = filter.tickSize as string;
          } else if (filter.filterType == "LOT_SIZE") {
            filters.stepSize = filter.stepSize as string;
            filters.minQty = filter.minQty as string;
            filters.maxQty = filter.maxQty as string;
          }
        }
        this.deliverySymbols[getSpotSymbol(symbolInfo.symbol)] = filters as DeliverySymbolInfo
        if (symbolInfo.pair.includes("ETH")) {
          // {
          //   symbol: 'ETHUSD_PERP', ETHUSD_230331
          //   pair: 'ETHUSD',
          //   pricePrecision: 2,
          //   quantityPrecision: 0,
          //   baseAsset: 'ETH',
          //   quoteAsset: 'USD',
          //   minPrice: '50',
          //   maxPrice: '306159',
          //   tickSize: '0.01',
          //   stepSize: '1',
          //   minQty: '1',
          //   maxQty: '1000000'
          // }
          // {
          //   symbol: 'BNBUSD_240329',
          //   pair: 'BNBUSD',
          //   contractType: 'NEXT_QUARTER',
          //   deliveryDate: 1711699200000,
          //   onboardDate: 1695974400000,
          //   contractStatus: 'TRADING',
          //   contractSize: 10,
          //   marginAsset: 'BNB',
          //   maintMarginPercent: '2.5000',
          //   requiredMarginPercent: '5.0000',
          //   baseAsset: 'BNB',
          //   quoteAsset: 'USD',
          // }
        }
      }
      return this.deliverySymbols[deliverySymbol];
    } catch (error) {
      outLogger.error('getDeliverySymolInfo', error.message)
    }
  }
  getFuturesPrice = singlePromise(async (symbol: string) => {
    const futuresPrices = await retryPromise(3, () => this.futureClient.getSymbolPriceTicker({ symbol }))
    if (Array.isArray(futuresPrices)) {
      for (let i = 0; i < futuresPrices.length; i++) {
        const futurePrice = futuresPrices[i];
        if (futurePrice.symbol === symbol) {
          return Number(futurePrice.price);
        }
      }
    } else {
      return Number(futuresPrices.price)
    }
  }, (arg) => arg);

  getDeliveryPrice = singlePromise(async (symbol: string) => {
    const deliveryPrices = await retryPromise(3, () => this.deliveryClient.getSymbolPriceTicker({ symbol }))
    if (Array.isArray(deliveryPrices)) {
      for (let i = 0; i < deliveryPrices.length; i++) {
        const deliveryPrice = deliveryPrices[i];
        if (deliveryPrice.symbol === symbol) {
          return Number(deliveryPrice.price);
        }
      }
    } else {
      return Number(deliveryPrices.price)
    }
  }, (arg) => arg);

  futuresDepth = singlePromise(async (symbol: string) => {
    return await retryPromise(2, () => this.futureClient.getOrderBook({
      symbol,
      limit: 500
    }))
  }, (arg) => arg);

  deliveryDepth = singlePromise(async (symbol: string) => {
    return await retryPromise(2, () => this.deliveryClient.getOrderBook({
      symbol,
      limit: 500
    }))
  }, (arg) => arg);

  getCandles (params:Parameters< MainClient['getKlines']>[0]) {
    return retryPromise(3, () => this.mainClient.getKlines(params)).then((list) => {
      // [
      //   [
      //     1499040000000,      // k线开盘时间
      //     "0.01634790",       // 开盘价
      //     "0.80000000",       // 最高价
      //     "0.01575800",       // 最低价
      //     "0.01577100",       // 收盘价(当前K线未结束的即为最新价)
      //     "148976.11427815",  // 成交量
      //     1499644799999,      // k线收盘时间
      //     "2434.19055334",    // 成交额
      //     308,                // 成交笔数
      //     "1756.87402397",    // 主动买入成交量
      //     "28.46694368",      // 主动买入成交额
      //     "17928899.62484339" // 请忽略该参数
      //   ]
      // ]
      if (list && list.length) {
        return list.map(([openTime, open, high, low, close, volume, closeTime, quoteVolume, count, takerBuyBaseAssetVolume, takerBuyQuoteAssetVolume]) => {
          return {
            openTime, open, high, low, close, volume, closeTime, quoteVolume, count, takerBuyBaseAssetVolume, takerBuyQuoteAssetVolume
          }
        })
      }
      return []
    }).catch((err) => {
      outLogger.error('getCandles', err.message ? err.message : err);
      return []
    })
  }
}


export const binanceCommon = new BinanceCommon();